/*
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package jmmos.gameserver.model.actor;

import static jmmos.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;

import jmmos.Config;
import jmmos.gameserver.SevenSigns;
import jmmos.gameserver.SevenSignsFestival;
import jmmos.gameserver.ThreadPoolManager;
import jmmos.gameserver.cache.HtmCache;
import jmmos.gameserver.datatables.ItemTable;
import jmmos.gameserver.handler.BypassHandler;
import jmmos.gameserver.handler.IBypassHandler;
import jmmos.gameserver.instancemanager.CHSiegeManager;
import jmmos.gameserver.instancemanager.CastleManager;
import jmmos.gameserver.instancemanager.FortManager;
import jmmos.gameserver.instancemanager.TownManager;
import jmmos.gameserver.instancemanager.WalkingManager;
import jmmos.gameserver.model.MMONpcAIData;
import jmmos.gameserver.model.MMOObject;
import jmmos.gameserver.model.MMOSpawn;
import jmmos.gameserver.model.MMOWorld;
import jmmos.gameserver.model.MMOWorldRegion;
import jmmos.gameserver.model.ShotType;
import jmmos.gameserver.model.actor.instance.MMOGuildHallManagerInstance;
import jmmos.gameserver.model.actor.instance.MMODoormenInstance;
import jmmos.gameserver.model.actor.instance.MMOFestivalGuideInstance;
import jmmos.gameserver.model.actor.instance.MMOFishermanInstance;
import jmmos.gameserver.model.actor.instance.MMOMerchantInstance;
import jmmos.gameserver.model.actor.instance.MMONpcInstance;
import jmmos.gameserver.model.actor.instance.MMOPcInstance;
import jmmos.gameserver.model.actor.instance.MMOTeleporterInstance;
import jmmos.gameserver.model.actor.instance.MMOTrainerHealersInstance;
import jmmos.gameserver.model.actor.instance.MMOTrainerInstance;
import jmmos.gameserver.model.actor.instance.MMOWarehouseInstance;
import jmmos.gameserver.model.actor.knownlist.NpcKnownList;
import jmmos.gameserver.model.actor.stat.NpcStat;
import jmmos.gameserver.model.actor.status.NpcStatus;
import jmmos.gameserver.model.actor.templates.MMONpcTemplate;
import jmmos.gameserver.model.actor.templates.MMONpcTemplate.AIType;
import jmmos.gameserver.model.entity.Castle;
import jmmos.gameserver.model.entity.Fort;
import jmmos.gameserver.model.entity.guildhall.SiegableHall;
import jmmos.gameserver.model.items.MMOItem;
import jmmos.gameserver.model.items.MMOWeapon;
import jmmos.gameserver.model.items.instance.MMOItemInstance;
import jmmos.gameserver.model.olympiad.Olympiad;
import jmmos.gameserver.model.quest.Quest;
import jmmos.gameserver.model.quest.Quest.QuestEventType;
import jmmos.gameserver.model.skills.MMOSkill;
import jmmos.gameserver.model.skills.targets.MMOTargetType;
import jmmos.gameserver.model.zone.type.MMOTownZone;
import jmmos.gameserver.network.SystemMessageId;
import jmmos.gameserver.network.serverpackets.AbstractNpcInfo;
import jmmos.gameserver.network.serverpackets.ActionFailed;
import jmmos.gameserver.network.serverpackets.ExChangeNpcState;
import jmmos.gameserver.network.serverpackets.MagicSkillUse;
import jmmos.gameserver.network.serverpackets.NpcHtmlMessage;
import jmmos.gameserver.network.serverpackets.ServerObjectInfo;
import jmmos.gameserver.network.serverpackets.SocialAction;
import jmmos.gameserver.taskmanager.DecayTaskManager;
import jmmos.gameserver.util.Broadcast;
import jmmos.util.Rnd;
import jmmos.util.StringUtil;

/**
 * This class represents a Non-Player-Character in the world.<br>
 * It can be a monster or a friendly character.<br>
 * It also uses a template to fetch some static values.<br>
 * The templates are hardcoded in the client, so we can rely on them.<br>
 * MMONpc:
 * <ul>
 * <li>MMOAttackable</li>
 * <li>MMOBoxInstance</li>
 * </ul>
 */
public class MMONpc extends MMOCharacter
{
	/** The interaction distance of the MMONpcInstance(is used as offset in MovetoLocation method) */
	public static final int INTERACTION_DISTANCE = 150;
	
	/** The MMOSpawn object that manage this MMONpcInstance */
	private MMOSpawn _spawn;
	
	/** The flag to specify if this MMONpcInstance is busy */
	private boolean _isBusy = false;
	
	/** The busy message for this MMONpcInstance */
	private String _busyMessage = "";
	
	/** True if endDecayTask has already been called */
	volatile boolean _isDecayed = false;
	
	/** The castle index in the array of MMOCastle this MMONpcInstance belongs to */
	private int _castleIndex = -2;
	
	/** The fortress index in the array of MMOFort this MMONpcInstance belongs to */
	private int _fortIndex = -2;
	
	public boolean isEventMob = false;
	private boolean _isInTown = false;
	
	/** True if this MMONpc is autoattackable **/
	private boolean _isAutoAttackable = false;
	
	/** Time of last social packet broadcast */
	private long _lastSocialBroadcast = 0;
	
	/** Minimum interval between social packets */
	private final int _minimalSocialInterval = 6000;
	
	protected RandomAnimationTask _rAniTask = null;
	private int _currentLHandId; // normally this shouldn't change from the template, but there exist exceptions
	private int _currentRHandId; // normally this shouldn't change from the template, but there exist exceptions
	private int _currentEnchant; // normally this shouldn't change from the template, but there exist exceptions
	private double _currentCollisionHeight; // used for npc grow effect skills
	private double _currentCollisionRadius; // used for npc grow effect skills
	
	private int _soulshotamount = 0;
	private int _spiritshotamount = 0;
	private int _displayEffect = 0;
	private int _scriptVal = 0;
	
	/**
	 * The character that summons this NPC.
	 */
	private MMOCharacter _summoner = null;
	
	private final MMONpcAIData _staticAIData = getTemplate().getAIDataStatic();
	
	private int _shotsMask = 0;
	
	public int getSoulShotChance()
	{
		return _staticAIData.getSoulShotChance();
	}
	
	public int getSpiritShotChance()
	{
		return _staticAIData.getSpiritShotChance();
	}
	
	public int getEnemyRange()
	{
		return _staticAIData.getEnemyRange();
	}
	
	public String getEnemyGuild()
	{
		return _staticAIData.getEnemyGuild();
	}
	
	public int getGuildRange()
	{
		return _staticAIData.getGuildRange();
	}
	
	public String getGuild()
	{
		return _staticAIData.getGuild();
	}
	
	/**
	 * @return the primary attack.
	 */
	public int getPrimarySkillId()
	{
		return _staticAIData.getPrimarySkillId();
	}
	
	public int getMinSkillChance()
	{
		return _staticAIData.getMinSkillChance();
	}
	
	public int getMaxSkillChance()
	{
		return _staticAIData.getMaxSkillChance();
	}
	
	public int getCanMove()
	{
		return _staticAIData.getCanMove();
	}
	
	public int getIsChaos()
	{
		return _staticAIData.getIsChaos();
	}
	
	public int getCanDodge()
	{
		return _staticAIData.getDodge();
	}
	
	public int getSSkillChance()
	{
		return _staticAIData.getShortRangeChance();
	}
	
	public int getLSkillChance()
	{
		return _staticAIData.getLongRangeChance();
	}
	
	public int getSwitchRangeChance()
	{
		return _staticAIData.getSwitchRangeChance();
	}
	
	public boolean hasLSkill()
	{
		if (_staticAIData.getLongRangeSkill() == 0)
		{
			return false;
		}
		return true;
	}
	
	public boolean hasSSkill()
	{
		if (_staticAIData.getShortRangeSkill() == 0)
		{
			return false;
		}
		return true;
	}
	
	public List<MMOSkill> getLongRangeSkill()
	{
		final List<MMOSkill> skilldata = new ArrayList<>();
		if ((_staticAIData == null) || (_staticAIData.getLongRangeSkill() == 0))
		{
			return skilldata;
		}
		
		switch (_staticAIData.getLongRangeSkill())
		{
			case -1:
			{
				Collection<MMOSkill> skills = getAllSkills();
				if (skills != null)
				{
					for (MMOSkill sk : skills)
					{
						if ((sk == null) || sk.isPassive() || (sk.getTargetType() == MMOTargetType.TARGET_SELF))
						{
							continue;
						}
						
						if (sk.getCastRange() >= 200)
						{
							skilldata.add(sk);
						}
					}
				}
				break;
			}
			case 1:
			{
				if (getTemplate().getUniversalSkills() != null)
				{
					for (MMOSkill sk : getTemplate().getUniversalSkills())
					{
						if (sk.getCastRange() >= 200)
						{
							skilldata.add(sk);
						}
					}
				}
				break;
			}
			default:
			{
				for (MMOSkill sk : getAllSkills())
				{
					if (sk.getId() == _staticAIData.getLongRangeSkill())
					{
						skilldata.add(sk);
					}
				}
			}
		}
		return skilldata;
	}
	
	public List<MMOSkill> getShortRangeSkill()
	{
		final List<MMOSkill> skilldata = new ArrayList<>();
		if ((_staticAIData == null) || (_staticAIData.getShortRangeSkill() == 0))
		{
			return skilldata;
		}
		
		switch (_staticAIData.getShortRangeSkill())
		{
			case -1:
			{
				Collection<MMOSkill> skills = getAllSkills();
				if (skills != null)
				{
					for (MMOSkill sk : skills)
					{
						if ((sk == null) || sk.isPassive() || (sk.getTargetType() == MMOTargetType.TARGET_SELF))
						{
							continue;
						}
						if (sk.getCastRange() <= 200)
						{
							skilldata.add(sk);
						}
					}
				}
				break;
			}
			case 1:
			{
				if (getTemplate().getUniversalSkills() != null)
				{
					for (MMOSkill sk : getTemplate().getUniversalSkills())
					{
						if (sk.getCastRange() <= 200)
						{
							skilldata.add(sk);
						}
					}
				}
				break;
			}
			default:
			{
				for (MMOSkill sk : getAllSkills())
				{
					if (sk.getId() == _staticAIData.getShortRangeSkill())
					{
						skilldata.add(sk);
					}
				}
			}
		}
		return skilldata;
	}
	
	/** Task launching the function onRandomAnimation() */
	protected class RandomAnimationTask implements Runnable
	{
		@Override
		public void run()
		{
			try
			{
				if (this != _rAniTask)
				{
					return; // Shouldn't happen, but who knows... just to make sure every active npc has only one timer.
				}
				if (isMob())
				{
					// Cancel further animation timers until intention is changed to ACTIVE again.
					if (getAI().getIntention() != AI_INTENTION_ACTIVE)
					{
						return;
					}
				}
				else
				{
					if (!isInActiveRegion())
					{
						return;
					}
				}
				
				if (!(isDead() || isStunned() || isSleeping() || isParalyzed()))
				{
					onRandomAnimation(Rnd.get(2, 3));
				}
				
				startRandomAnimationTimer();
			}
			catch (Exception e)
			{
				_log.log(Level.SEVERE, "", e);
			}
		}
	}
	
	/**
	 * Send a packet SocialAction to all MMOPcInstance in the _KnownPlayers of the MMONpcInstance and create a new RandomAnimation Task.
	 * @param animationId
	 */
	public void onRandomAnimation(int animationId)
	{
		// Send a packet SocialAction to all MMOPcInstance in the _KnownPlayers of the MMONpcInstance
		long now = System.currentTimeMillis();
		if ((now - _lastSocialBroadcast) > _minimalSocialInterval)
		{
			_lastSocialBroadcast = now;
			broadcastPacket(new SocialAction(getObjectId(), animationId));
		}
	}
	
	/**
	 * Create a RandomAnimation Task that will be launched after the calculated delay.
	 */
	public void startRandomAnimationTimer()
	{
		if (!hasRandomAnimation())
		{
			return;
		}
		
		int minWait = isMob() ? Config.MIN_MONSTER_ANIMATION : Config.MIN_NPC_ANIMATION;
		int maxWait = isMob() ? Config.MAX_MONSTER_ANIMATION : Config.MAX_NPC_ANIMATION;
		
		// Calculate the delay before the next animation
		int interval = Rnd.get(minWait, maxWait) * 1000;
		
		// Create a RandomAnimation Task that will be launched after the calculated delay
		_rAniTask = new RandomAnimationTask();
		ThreadPoolManager.getInstance().scheduleGeneral(_rAniTask, interval);
	}
	
	/**
	 * @return true if the server allows Random Animation.
	 */
	public boolean hasRandomAnimation()
	{
		return ((Config.MAX_NPC_ANIMATION > 0) && !getAiType().equals(AIType.CORPSE));
	}
	
	/**
	 * Constructor of MMONpcInstance (use MMOCharacter constructor).<br>
	 * <B><U>Actions</U>:</B>
	 * <ul>
	 * <li>Call the MMOCharacter constructor to set the _template of the MMOCharacter (copy skills from template to object and link _calculators to NPC_STD_CALCULATOR)</li>
	 * <li>Set the name of the MMOCharacter</li>
	 * <li>Create a RandomAnimation Task that will be launched after the calculated delay if the server allow it</li>
	 * </ul>
	 * @param objectId Identifier of the object to initialized
	 * @param template The MMONpcTemplate to apply to the NPC
	 */
	public MMONpc(int objectId, MMONpcTemplate template)
	{
		// Call the MMOCharacter constructor to set the _template of the MMOCharacter, copy skills from template to object
		// and link _calculators to NPC_STD_CALCULATOR
		super(objectId, template);
		setInstanceType(InstanceType.MMONpc);
		initCharStatusUpdateValues();
		
		// initialize the "current" equipment
		_currentLHandId = getTemplate().getLeftHand();
		_currentRHandId = getTemplate().getRightHand();
		_currentEnchant = Config.ENABLE_RANDOM_ENCHANT_EFFECT ? Rnd.get(4, 21) : getTemplate().getEnchantEffect();
		
		// initialize the "current" collisions
		_currentCollisionHeight = getTemplate().getfCollisionHeight();
		_currentCollisionRadius = getTemplate().getfCollisionRadius();
		
		if (template == null)
		{
			_log.severe("No template for Npc. Please check your datapack is setup correctly.");
			return;
		}
		
		// Set the name of the MMOCharacter
		setName(template.getName());
	}
	
	@Override
	public NpcKnownList getKnownList()
	{
		return (NpcKnownList) super.getKnownList();
	}
	
	@Override
	public void initKnownList()
	{
		setKnownList(new NpcKnownList(this));
	}
	
	@Override
	public NpcStat getStat()
	{
		return (NpcStat) super.getStat();
	}
	
	@Override
	public void initCharStat()
	{
		setStat(new NpcStat(this));
	}
	
	@Override
	public NpcStatus getStatus()
	{
		return (NpcStatus) super.getStatus();
	}
	
	@Override
	public void initCharStatus()
	{
		setStatus(new NpcStatus(this));
	}
	
	/** Return the MMONpcTemplate of the MMONpcInstance. */
	@Override
	public final MMONpcTemplate getTemplate()
	{
		return (MMONpcTemplate) super.getTemplate();
	}
	
	/**
	 * @return the generic Identifier of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public int getNpcId()
	{
		return getTemplate().getNpcId();
	}
	
	@Override
	public boolean isAttackable()
	{
		return Config.ALT_ATTACKABLE_NPCS;
	}
	
	/**
	 * <B><U> Concept</U> :</B><br>
	 * If a NPC belongs to a Faction, other NPC of the faction inside the Faction range will help it if it's attacked
	 * @return the faction Identifier of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public final String getFactionId()
	{
		return getGuild();
	}
	
	/**
	 * Return the Level of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	@Override
	public final int getLevel()
	{
		return getTemplate().getLevel();
	}
	
	/**
	 * @return True if the MMONpcInstance is aggressive (ex : MMOMonsterInstance in function of aggroRange).
	 */
	public boolean isAggressive()
	{
		return false;
	}
	
	/**
	 * @return the Aggro Range of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public int getAggroRange()
	{
		return _staticAIData.getAggroRange();
	}
	
	/**
	 * @return the Faction Range of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public int getFactionRange()
	{
		return getGuildRange();
	}
	
	/**
	 * Return True if this MMONpcInstance is undead in function of the MMONpcTemplate.
	 */
	@Override
	public boolean isUndead()
	{
		return getTemplate().isUndead();
	}
	
	/**
	 * Send a packet NpcInfo with state of abnormal effect to all MMOPcInstance in the _KnownPlayers of the MMONpcInstance.
	 */
	@Override
	public void updateAbnormalEffect()
	{
		// Send a Server->Client packet NpcInfo with state of abnormal effect to all MMOPcInstance in the _KnownPlayers of the MMONpcInstance
		Collection<MMOPcInstance> plrs = getKnownList().getKnownPlayers().values();
		for (MMOPcInstance player : plrs)
		{
			if (player == null)
			{
				continue;
			}
			if (getRunSpeed() == 0)
			{
				player.sendPacket(new ServerObjectInfo(this, player));
			}
			else
			{
				player.sendPacket(new AbstractNpcInfo.NpcInfo(this, player));
			}
		}
	}
	
	/**
	 * <B><U>Values </U>:</B>
	 * <ul>
	 * <li>object is a MMOFolkInstance : 0 (don't remember it)</li>
	 * <li>object is a MMOCharacter : 0 (don't remember it)</li>
	 * <li>object is a MMOPlayable : 1500</li>
	 * <li>others : 500</li>
	 * <ul>
	 * @param object The Object to add to _knownObject
	 * @return the distance under which the object must be add to _knownObject in function of the object type.
	 */
	public int getDistanceToWatchObject(MMOObject object)
	{
		if (object instanceof MMOFestivalGuideInstance)
		{
			return 10000;
		}
		
		if ((object instanceof MMONpcInstance) || !(object instanceof MMOCharacter))
		{
			return 0;
		}
		
		if (object instanceof MMOPlayable)
		{
			return 1500;
		}
		
		return 500;
	}
	
	/**
	 * <B><U>Values</U>:</B>
	 * <ul>
	 * <li>object is not a MMOCharacter : 0 (don't remember it)</li>
	 * <li>object is a MMOFolkInstance : 0 (don't remember it)</li>
	 * <li>object is a MMOPlayable : 3000</li>
	 * <li>others : 1000</li>
	 * </ul>
	 * @param object The Object to remove from _knownObject
	 * @return the distance after which the object must be remove from _knownObject in function of the object type.
	 */
	public int getDistanceToForgetObject(MMOObject object)
	{
		return 2 * getDistanceToWatchObject(object);
	}
	
	@Override
	public boolean isAutoAttackable(MMOCharacter attacker)
	{
		return _isAutoAttackable;
	}
	
	public void setAutoAttackable(boolean flag)
	{
		_isAutoAttackable = flag;
	}
	
	/**
	 * @return the Identifier of the item in the left hand of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public int getLeftHandItem()
	{
		return _currentLHandId;
	}
	
	/**
	 * @return the Identifier of the item in the right hand of this MMONpcInstance contained in the MMONpcTemplate.
	 */
	public int getRightHandItem()
	{
		return _currentRHandId;
	}
	
	public int getEnchantEffect()
	{
		return _currentEnchant;
	}
	
	/**
	 * @return the busy status of this MMONpcInstance.
	 */
	public final boolean isBusy()
	{
		return _isBusy;
	}
	
	/**
	 * @param isBusy the busy status of this MMONpc
	 */
	public void setBusy(boolean isBusy)
	{
		_isBusy = isBusy;
	}
	
	/**
	 * @return the busy message of this MMONpcInstance.
	 */
	public final String getBusyMessage()
	{
		return _busyMessage;
	}
	
	/**
	 * @param message the busy message of this MMONpc.
	 */
	public void setBusyMessage(String message)
	{
		_busyMessage = message;
	}
	
	/**
	 * @return true if this MMONpc instance can be warehouse manager.
	 */
	public boolean isWarehouse()
	{
		return false;
	}
	
	public boolean canTarget(MMOPcInstance player)
	{
		if (player.isOutOfControl())
		{
			player.sendPacket(ActionFailed.STATIC_PACKET);
			return false;
		}
		if (player.isLockedTarget() && (player.getLockedTarget() != this))
		{
			player.sendPacket(SystemMessageId.FAILED_CHANGE_TARGET);
			player.sendPacket(ActionFailed.STATIC_PACKET);
			return false;
		}
		// TODO: More checks...
		
		return true;
	}
	
	public boolean canInteract(MMOPcInstance player)
	{
		if (player.isCastingNow() || player.isCastingSimultaneouslyNow())
		{
			return false;
		}
		if (player.isDead() || player.isFakeDeath())
		{
			return false;
		}
		if (player.isSitting())
		{
			return false;
		}
		if (player.getPrivateStoreType() != MMOPcInstance.STORE_PRIVATE_NONE)
		{
			return false;
		}
		if (!isInsideRadius(player, INTERACTION_DISTANCE, true, false))
		{
			return false;
		}
		if ((player.getInstanceId() != getInstanceId()) && (player.getInstanceId() != -1))
		{
			return false;
		}
		if (isBusy())
		{
			return false;
		}
		return true;
	}
	
	/**
	 * @return the MMOCastle this MMONpcInstance belongs to.
	 */
	public final Castle getCastle()
	{
		// Get castle this NPC belongs to (excluding MMOAttackable)
		if (_castleIndex < 0)
		{
			MMOTownZone town = TownManager.getTown(getX(), getY(), getZ());
			
			if (town != null)
			{
				_castleIndex = CastleManager.getInstance().getCastleIndex(town.getTaxById());
			}
			
			if (_castleIndex < 0)
			{
				_castleIndex = CastleManager.getInstance().findNearestCastleIndex(this);
			}
			else
			{
				_isInTown = true; // Npc was spawned in town
			}
		}
		
		if (_castleIndex < 0)
		{
			return null;
		}
		
		return CastleManager.getInstance().getCastles().get(_castleIndex);
	}
	
	/**
	 * Verify if the given player is this NPC's lord.
	 * @param player the player to check
	 * @return {@code true} if the player is guild leader and owner of a castle of fort that this NPC belongs to, {@code false} otherwise
	 */
	public boolean isMyLord(MMOPcInstance player)
	{
		if (player.isGuildLeader())
		{
			final int castleId = getCastle() != null ? getCastle().getCastleId() : -1;
			final int fortId = getFort() != null ? getFort().getFortId() : -1;
			return (player.getGuild().getCastleId() == castleId) || (player.getGuild().getFortId() == fortId);
		}
		return false;
	}
	
	public final SiegableHall getConquerableHall()
	{
		return CHSiegeManager.getInstance().getNearbyGuildHall(getX(), getY(), 10000);
	}
	
	/**
	 * Return closest castle in defined distance
	 * @param maxDistance long
	 * @return Castle
	 */
	public final Castle getCastle(long maxDistance)
	{
		int index = CastleManager.getInstance().findNearestCastleIndex(this, maxDistance);
		
		if (index < 0)
		{
			return null;
		}
		
		return CastleManager.getInstance().getCastles().get(index);
	}
	
	/**
	 * @return the MMOFort this MMONpcInstance belongs to.
	 */
	public final Fort getFort()
	{
		// Get Fort this NPC belongs to (excluding MMOAttackable)
		if (_fortIndex < 0)
		{
			Fort fort = FortManager.getInstance().getFort(getX(), getY(), getZ());
			if (fort != null)
			{
				_fortIndex = FortManager.getInstance().getFortIndex(fort.getFortId());
			}
			
			if (_fortIndex < 0)
			{
				_fortIndex = FortManager.getInstance().findNearestFortIndex(this);
			}
		}
		
		if (_fortIndex < 0)
		{
			return null;
		}
		
		return FortManager.getInstance().getForts().get(_fortIndex);
	}
	
	/**
	 * Return closest Fort in defined distance
	 * @param maxDistance long
	 * @return Fort
	 */
	public final Fort getFort(long maxDistance)
	{
		int index = FortManager.getInstance().findNearestFortIndex(this, maxDistance);
		
		if (index < 0)
		{
			return null;
		}
		
		return FortManager.getInstance().getForts().get(index);
	}
	
	public final boolean getIsInTown()
	{
		if (_castleIndex < 0)
		{
			getCastle();
		}
		
		return _isInTown;
	}
	
	/**
	 * Open a quest or chat window on client with the text of the MMONpcInstance in function of the command.<br>
	 * <B><U> Example of use </U> :</B>
	 * <ul>
	 * <li>Client packet : RequestBypassToServer</li>
	 * </ul>
	 * @param player
	 * @param command The command string received from client
	 */
	public void onBypassFeedback(MMOPcInstance player, String command)
	{
		// if (canInteract(player))
		{
			if (isBusy() && (getBusyMessage().length() > 0))
			{
				player.sendPacket(ActionFailed.STATIC_PACKET);
				
				NpcHtmlMessage html = new NpcHtmlMessage(getObjectId());
				html.setFile(player.getHtmlPrefix(), "data/html/npcbusy.htm");
				html.replace("%busymessage%", getBusyMessage());
				html.replace("%npcname%", getName());
				html.replace("%playername%", player.getName());
				player.sendPacket(html);
			}
			else
			{
				IBypassHandler handler = BypassHandler.getInstance().getHandler(command);
				if (handler != null)
				{
					handler.useBypass(command, player, this);
				}
				else
				{
					_log.info(getClass().getSimpleName() + ": Unknown NPC bypass: \"" + command + "\" NpcId: " + getNpcId());
				}
			}
		}
	}
	
	/**
	 * Return null (regular NPCs don't have weapons instancies).
	 */
	@Override
	public MMOItemInstance getActiveWeaponInstance()
	{
		// regular NPCs dont have weapons instancies
		return null;
	}
	
	/**
	 * Return the weapon item equiped in the right hand of the MMONpcInstance or null.
	 */
	@Override
	public MMOWeapon getActiveWeaponItem()
	{
		// Get the weapon identifier equiped in the right hand of the MMONpcInstance
		int weaponId = getTemplate().getRightHand();
		
		if (weaponId < 1)
		{
			return null;
		}
		
		// Get the weapon item equiped in the right hand of the MMONpcInstance
		MMOItem item = ItemTable.getInstance().getTemplate(getTemplate().getRightHand());
		
		if (!(item instanceof MMOWeapon))
		{
			return null;
		}
		
		return (MMOWeapon) item;
	}
	
	/**
	 * Return null (regular NPCs don't have weapons instancies).
	 */
	@Override
	public MMOItemInstance getSecondaryWeaponInstance()
	{
		// regular NPCs dont have weapons instancies
		return null;
	}
	
	/**
	 * Return the weapon item equiped in the left hand of the MMONpcInstance or null.
	 */
	@Override
	public MMOWeapon getSecondaryWeaponItem()
	{
		// Get the weapon identifier equiped in the right hand of the MMONpcInstance
		int weaponId = getTemplate().getLeftHand();
		
		if (weaponId < 1)
		{
			return null;
		}
		
		// Get the weapon item equiped in the right hand of the MMONpcInstance
		MMOItem item = ItemTable.getInstance().getTemplate(getTemplate().getLeftHand());
		
		if (!(item instanceof MMOWeapon))
		{
			return null;
		}
		
		return (MMOWeapon) item;
	}
	
	/**
	 * Send a Server->Client packet NpcHtmlMessage to the MMOPcInstance in order to display the message of the MMONpcInstance.
	 * @param player The MMOPcInstance who talks with the MMONpcInstance
	 * @param content The text of the MMONpcMessage
	 */
	public void insertObjectIdAndShowChatWindow(MMOPcInstance player, String content)
	{
		// Send a Server->Client packet NpcHtmlMessage to the MMOPcInstance in order to display the message of the MMONpcInstance
		content = content.replaceAll("%objectId%", String.valueOf(getObjectId()));
		NpcHtmlMessage npcReply = new NpcHtmlMessage(getObjectId());
		npcReply.setHtml(content);
		player.sendPacket(npcReply);
	}
	
	/**
	 * <B><U Format of the pathfile</U>:</B>
	 * <ul>
	 * <li>if the file exists on the server (page number = 0) : <B>data/html/default/12006.htm</B> (npcId-page number)</li>
	 * <li>if the file exists on the server (page number > 0) : <B>data/html/default/12006-1.htm</B> (npcId-page number)</li>
	 * <li>if the file doesn't exist on the server : <B>data/html/npcdefault.htm</B> (message : "I have nothing to say to you")</li>
	 * </ul>
	 * @param npcId The Identifier of the MMONpcInstance whose text must be display
	 * @param val The number of the page to display
	 * @return the pathfile of the selected HTML file in function of the npcId and of the page number.
	 */
	public String getHtmlPath(int npcId, int val)
	{
		String pom = "";
		
		if (val == 0)
		{
			pom = "" + npcId;
		}
		else
		{
			pom = npcId + "-" + val;
		}
		
		String temp = "data/html/default/" + pom + ".htm";
		
		if (!Config.LAZY_CACHE)
		{
			// If not running lazy cache the file must be in the cache or it doesnt exist
			if (HtmCache.getInstance().contains(temp))
			{
				return temp;
			}
		}
		else
		{
			if (HtmCache.getInstance().isLoadable(temp))
			{
				return temp;
			}
		}
		
		// If the file is not found, the standard message "I have nothing to say to you" is returned
		return "data/html/npcdefault.htm";
	}
	
	public void showChatWindow(MMOPcInstance player)
	{
		showChatWindow(player, 0);
	}
	
	/**
	 * Returns true if html exists
	 * @param player
	 * @param type
	 * @return boolean
	 */
	private boolean showPkDenyChatWindow(MMOPcInstance player, String type)
	{
		String html = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/" + type + "/" + getNpcId() + "-pk.htm");
		
		if (html != null)
		{
			NpcHtmlMessage pkDenyMsg = new NpcHtmlMessage(getObjectId());
			pkDenyMsg.setHtml(html);
			player.sendPacket(pkDenyMsg);
			player.sendPacket(ActionFailed.STATIC_PACKET);
			return true;
		}
		
		return false;
	}
	
	/**
	 * Open a chat window on client with the text of the MMONpcInstance.<br>
	 * <B><U>Actions</U>:</B>
	 * <ul>
	 * <li>Get the text of the selected HTML file in function of the npcId and of the page number</li>
	 * <li>Send a Server->Client NpcHtmlMessage containing the text of the MMONpcInstance to the MMOPcInstance</li>
	 * <li>Send a Server->Client ActionFailed to the MMOPcInstance in order to avoid that the client wait another packet</li>
	 * </ul>
	 * @param player The MMOPcInstance that talk with the MMONpcInstance
	 * @param val The number of the page of the MMONpcInstance to display
	 */
	public void showChatWindow(MMOPcInstance player, int val)
	{
		if (Config.NON_TALKING_NPCS.contains(getNpcId()))
		{
			player.sendPacket(ActionFailed.STATIC_PACKET);
			return;
		}
		if (player.isCursedWeaponEquipped() && (!(player.getTarget() instanceof MMOGuildHallManagerInstance) || !(player.getTarget() instanceof MMODoormenInstance)))
		{
			player.setTarget(player);
			return;
		}
		if (player.getKarma() > 0)
		{
			if (!Config.ALT_GAME_KARMA_PLAYER_CAN_SHOP && (this instanceof MMOMerchantInstance))
			{
				if (showPkDenyChatWindow(player, "merchant"))
				{
					return;
				}
			}
			else if (!Config.ALT_GAME_KARMA_PLAYER_CAN_USE_GK && (this instanceof MMOTeleporterInstance))
			{
				if (showPkDenyChatWindow(player, "teleporter"))
				{
					return;
				}
			}
			else if (!Config.ALT_GAME_KARMA_PLAYER_CAN_USE_WAREHOUSE && (this instanceof MMOWarehouseInstance))
			{
				if (showPkDenyChatWindow(player, "warehouse"))
				{
					return;
				}
			}
			else if (!Config.ALT_GAME_KARMA_PLAYER_CAN_SHOP && (this instanceof MMOFishermanInstance))
			{
				if (showPkDenyChatWindow(player, "fisherman"))
				{
					return;
				}
			}
		}
		
		if (getTemplate().isType("MMOAuctioneer") && (val == 0))
		{
			return;
		}
		
		int npcId = getTemplate().getNpcId();
		
		/* For use with Seven Signs implementation */
		String filename = SevenSigns.SEVEN_SIGNS_HTML_PATH;
		int sealAvariceOwner = SevenSigns.getInstance().getSealOwner(SevenSigns.SEAL_AVARICE);
		int sealGnosisOwner = SevenSigns.getInstance().getSealOwner(SevenSigns.SEAL_GNOSIS);
		int playerCabal = SevenSigns.getInstance().getPlayerCabal(player.getObjectId());
		int compWinner = SevenSigns.getInstance().getCabalHighestScore();
		
		switch (npcId)
		{
			case 31127: //
			case 31128: //
			case 31129: // Dawn Festival Guides
			case 31130: //
			case 31131: //
				filename += "festival/dawn_guide.htm";
				break;
			case 31137: //
			case 31138: //
			case 31139: // Dusk Festival Guides
			case 31140: //
			case 31141: //
				filename += "festival/dusk_guide.htm";
				break;
			case 31092: // Black Marketeer of Mammon
				filename += "blkmrkt_1.htm";
				break;
			case 31113: // Merchant of Mammon
				if (Config.ALT_STRICT_SEVENSIGNS)
				{
					switch (compWinner)
					{
						case SevenSigns.CABAL_DAWN:
							if ((playerCabal != compWinner) || (playerCabal != sealAvariceOwner))
							{
								player.sendPacket(SystemMessageId.CAN_BE_USED_BY_DAWN);
								player.sendPacket(ActionFailed.STATIC_PACKET);
								return;
							}
							break;
						case SevenSigns.CABAL_DUSK:
							if ((playerCabal != compWinner) || (playerCabal != sealAvariceOwner))
							{
								player.sendPacket(SystemMessageId.CAN_BE_USED_BY_DUSK);
								player.sendPacket(ActionFailed.STATIC_PACKET);
								return;
							}
							break;
						default:
							player.sendPacket(SystemMessageId.SSQ_COMPETITION_UNDERWAY);
							return;
					}
				}
				filename += "mammmerch_1.htm";
				break;
			case 31126: // Blacksmith of Mammon
				if (Config.ALT_STRICT_SEVENSIGNS)
				{
					switch (compWinner)
					{
						case SevenSigns.CABAL_DAWN:
							if ((playerCabal != compWinner) || (playerCabal != sealGnosisOwner))
							{
								player.sendPacket(SystemMessageId.CAN_BE_USED_BY_DAWN);
								player.sendPacket(ActionFailed.STATIC_PACKET);
								return;
							}
							break;
						case SevenSigns.CABAL_DUSK:
							if ((playerCabal != compWinner) || (playerCabal != sealGnosisOwner))
							{
								player.sendPacket(SystemMessageId.CAN_BE_USED_BY_DUSK);
								player.sendPacket(ActionFailed.STATIC_PACKET);
								return;
							}
							break;
						default:
							player.sendPacket(SystemMessageId.SSQ_COMPETITION_UNDERWAY);
							return;
					}
				}
				filename += "mammblack_1.htm";
				break;
			case 31132:
			case 31133:
			case 31134:
			case 31135:
			case 31136: // Festival Witches
			case 31142:
			case 31143:
			case 31144:
			case 31145:
			case 31146:
				filename += "festival/festival_witch.htm";
				break;
			case 31688:
				if (player.isNoble())
				{
					filename = Olympiad.OLYMPIAD_HTML_PATH + "noble_main.htm";
				}
				else
				{
					filename = (getHtmlPath(npcId, val));
				}
				break;
			case 31690:
			case 31769:
			case 31770:
			case 31771:
			case 31772:
				if (player.isHero() || player.isNoble())
				{
					filename = Olympiad.OLYMPIAD_HTML_PATH + "hero_main.htm";
				}
				else
				{
					filename = (getHtmlPath(npcId, val));
				}
				break;
			case 36402:
				if (player.olyBuff > 0)
				{
					filename = (player.olyBuff == 5 ? Olympiad.OLYMPIAD_HTML_PATH + "olympiad_buffs.htm" : Olympiad.OLYMPIAD_HTML_PATH + "olympiad_5buffs.htm");
				}
				else
				{
					filename = Olympiad.OLYMPIAD_HTML_PATH + "olympiad_nobuffs.htm";
				}
				break;
			case 30298: // Blacksmith Pinter
				if (player.isAcademyMember())
				{
					filename = (getHtmlPath(npcId, 1));
				}
				else
				{
					filename = (getHtmlPath(npcId, val));
				}
				break;
			default:
				if ((npcId >= 31865) && (npcId <= 31918))
				{
					if (val == 0)
					{
						filename += "rift/GuardianOfBorder.htm";
					}
					else
					{
						filename += "rift/GuardianOfBorder-" + val + ".htm";
					}
					break;
				}
				if (((npcId >= 31093) && (npcId <= 31094)) || ((npcId >= 31172) && (npcId <= 31201)) || ((npcId >= 31239) && (npcId <= 31254)))
				{
					return;
				}
				// Get the text of the selected HTML file in function of the npcId and of the page number
				filename = (getHtmlPath(npcId, val));
				break;
		}
		
		// Send a Server->Client NpcHtmlMessage containing the text of the MMONpcInstance to the MMOPcInstance
		NpcHtmlMessage html = new NpcHtmlMessage(getObjectId());
		html.setFile(player.getHtmlPrefix(), filename);
		
		if (this instanceof MMOMerchantInstance)
		{
			if (Config.LIST_PET_RENT_NPC.contains(npcId))
			{
				html.replace("_Quest", "_RentPet\">Rent Pet</a><br><a action=\"bypass -h npc_%objectId%_Quest");
			}
		}
		
		html.replace("%objectId%", String.valueOf(getObjectId()));
		html.replace("%festivalMins%", SevenSignsFestival.getInstance().getTimeToNextFestivalStr());
		player.sendPacket(html);
		
		// Send a Server->Client ActionFailed to the MMOPcInstance in order to avoid that the client wait another packet
		player.sendPacket(ActionFailed.STATIC_PACKET);
	}
	
	/**
	 * Open a chat window on client with the text specified by the given file name and path, relative to the datapack root.
	 * @param player The MMOPcInstance that talk with the MMONpcInstance
	 * @param filename The filename that contains the text to send
	 */
	public void showChatWindow(MMOPcInstance player, String filename)
	{
		// Send a Server->Client NpcHtmlMessage containing the text of the MMONpcInstance to the MMOPcInstance
		NpcHtmlMessage html = new NpcHtmlMessage(getObjectId());
		html.setFile(player.getHtmlPrefix(), filename);
		html.replace("%objectId%", String.valueOf(getObjectId()));
		player.sendPacket(html);
		
		// Send a Server->Client ActionFailed to the MMOPcInstance in order to avoid that the client wait another packet
		player.sendPacket(ActionFailed.STATIC_PACKET);
	}
	
	/**
	 * @return the Exp Reward of this MMONpcInstance contained in the MMONpcTemplate (modified by RATE_XP).
	 */
	public int getExpReward()
	{
		return (int) (getTemplate().getRewardExp() * Config.RATE_XP);
	}
	
	/**
	 * @return the SP Reward of this MMONpcInstance contained in the MMONpcTemplate (modified by RATE_SP).
	 */
	public int getSpReward()
	{
		return (int) (getTemplate().getRewardSp() * Config.RATE_SP);
	}
	
	/**
	 * Kill the MMONpcInstance (the corpse disappeared after 7 seconds).<br>
	 * <B><U>Actions</U>:</B>
	 * <ul>
	 * <li>Create a DecayTask to remove the corpse of the MMONpcInstance after 7 seconds</li>
	 * <li>Set target to null and cancel Attack or Cast</li>
	 * <li>Stop movement</li>
	 * <li>Stop HP/MP/CP Regeneration task</li>
	 * <li>Stop all active skills effects in progress on the MMOCharacter</li>
	 * <li>Send the Server->Client packet StatusUpdate with current HP and MP to all other MMOPcInstance to inform</li>
	 * <li>Notify MMOCharacter AI</li>
	 * </ul>
	 * @param killer The MMOCharacter who killed it
	 */
	@Override
	public boolean doDie(MMOCharacter killer)
	{
		if (!super.doDie(killer))
		{
			return false;
		}
		
		// normally this wouldn't really be needed, but for those few exceptions,
		// we do need to reset the weapons back to the initial template weapon.
		_currentLHandId = getTemplate().getLeftHand();
		_currentRHandId = getTemplate().getRightHand();
		_currentCollisionHeight = getTemplate().getfCollisionHeight();
		_currentCollisionRadius = getTemplate().getfCollisionRadius();
		DecayTaskManager.getInstance().addDecayTask(this);
		return true;
	}
	
	/**
	 * Set the spawn of the MMONpcInstance.
	 * @param spawn The MMOSpawn that manage the MMONpcInstance
	 */
	public void setSpawn(MMOSpawn spawn)
	{
		_spawn = spawn;
	}
	
	@Override
	public void onSpawn()
	{
		super.onSpawn();
		
		// Recharge shots
		_soulshotamount = getTemplate().getAIDataStatic().getSoulShot();
		_spiritshotamount = getTemplate().getAIDataStatic().getSpiritShot();
		
		if (getTemplate().getEventQuests(QuestEventType.ON_SPAWN) != null)
		{
			for (Quest quest : getTemplate().getEventQuests(QuestEventType.ON_SPAWN))
			{
				quest.notifySpawn(this);
			}
		}
	}
	
	/**
	 * Remove the MMONpcInstance from the world and update its spawn object (for a complete removal use the deleteMe method).<br>
	 * <B><U>Actions</U>:</B>
	 * <ul>
	 * <li>Remove the MMONpcInstance from the world when the decay task is launched</li>
	 * <li>Decrease its spawn counter</li>
	 * <li>Manage Siege task (killFlag, killCT)</li>
	 * </ul>
	 * <FONT COLOR=#FF0000><B> <U>Caution</U> : This method DOESN'T REMOVE the object from _allObjects of MMOWorld </B></FONT><BR>
	 * <FONT COLOR=#FF0000><B> <U>Caution</U> : This method DOESN'T SEND Server->Client packets to players</B></FONT>
	 */
	@Override
	public void onDecay()
	{
		if (isDecayed())
		{
			return;
		}
		setDecayed(true);
		
		// Remove the MMONpcInstance from the world when the decay task is launched
		super.onDecay();
		
		// Decrease its spawn counter
		if (_spawn != null)
		{
			_spawn.decreaseCount(this);
		}
		
		// Notify Walking Manager
		WalkingManager.getInstance().onDeath(this);
	}
	
	/**
	 * Remove PROPERLY the MMONpcInstance from the world.<br>
	 * <B><U>Actions</U>:</B>
	 * <ul>
	 * <li>Remove the MMONpcInstance from the world and update its spawn object</li>
	 * <li>Remove all MMOObject from _knownObjects and _knownPlayer of the MMONpcInstance then cancel Attack or Cast and notify AI</li>
	 * <li>Remove MMOObject object from _allObjects of MMOWorld</li>
	 * </ul>
	 * <FONT COLOR=#FF0000><B><U>Caution</U>: This method DOESN'T SEND Server->Client packets to players</B></FONT><br>
	 * UnAfraid: TODO: Add Listener here
	 */
	@Override
	public void deleteMe()
	{
		MMOWorldRegion oldRegion = getWorldRegion();
		
		try
		{
			onDecay();
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE, "Failed decayMe().", e);
		}
		try
		{
			if (_fusionSkill != null)
			{
				abortCast();
			}
			
			for (MMOCharacter character : getKnownList().getKnownCharacters())
			{
				if ((character.getFusionSkill() != null) && (character.getFusionSkill().getTarget() == this))
				{
					character.abortCast();
				}
			}
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE, "deleteMe()", e);
		}
		if (oldRegion != null)
		{
			oldRegion.removeFromZones(this);
		}
		
		// Remove all MMOObject from _knownObjects and _knownPlayer of the MMOCharacter then cancel Attack or Cast and notify AI
		try
		{
			getKnownList().removeAllKnownObjects();
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE, "Failed removing cleaning knownlist.", e);
		}
		
		// Remove MMOObject object from _allObjects of MMOWorld
		MMOWorld.getInstance().removeObject(this);
		
		super.deleteMe();
	}
	
	/**
	 * @return the MMOSpawn object that manage this MMONpcInstance.
	 */
	public MMOSpawn getSpawn()
	{
		return _spawn;
	}
	
	@Override
	public String toString()
	{
		return getClass().getSimpleName() + ":" + getName() + "(" + getNpcId() + ")" + "[" + getObjectId() + "]";
	}
	
	public boolean isDecayed()
	{
		return _isDecayed;
	}
	
	public void setDecayed(boolean decayed)
	{
		_isDecayed = decayed;
	}
	
	public void endDecayTask()
	{
		if (!isDecayed())
		{
			DecayTaskManager.getInstance().cancelDecayTask(this);
			onDecay();
		}
	}
	
	public boolean isMob() // rather delete this check
	{
		return false; // This means we use MAX_NPC_ANIMATION instead of MAX_MONSTER_ANIMATION
	}
	
	// Two functions to change the appearance of the equipped weapons on the NPC
	// This is only useful for a few NPCs and is most likely going to be called from AI
	public void setLHandId(int newWeaponId)
	{
		_currentLHandId = newWeaponId;
		updateAbnormalEffect();
	}
	
	public void setRHandId(int newWeaponId)
	{
		_currentRHandId = newWeaponId;
		updateAbnormalEffect();
	}
	
	public void setLRHandId(int newLWeaponId, int newRWeaponId)
	{
		_currentRHandId = newRWeaponId;
		_currentLHandId = newLWeaponId;
		updateAbnormalEffect();
	}
	
	public void setEnchant(int newEnchantValue)
	{
		_currentEnchant = newEnchantValue;
		updateAbnormalEffect();
	}
	
	public boolean isShowName()
	{
		return _staticAIData.showName();
	}
	
	@Override
	public boolean isTargetable()
	{
		return _staticAIData.isTargetable();
	}
	
	public void setCollisionHeight(double height)
	{
		_currentCollisionHeight = height;
	}
	
	public void setCollisionRadius(double radius)
	{
		_currentCollisionRadius = radius;
	}
	
	public double getCollisionHeight()
	{
		return _currentCollisionHeight;
	}
	
	public double getCollisionRadius()
	{
		return _currentCollisionRadius;
	}
	
	@Override
	public void sendInfo(MMOPcInstance activeChar)
	{
		if (Config.CHECK_KNOWN && activeChar.isGM())
		{
			activeChar.sendMessage("Added NPC: " + getName());
		}
		
		if (getRunSpeed() == 0)
		{
			activeChar.sendPacket(new ServerObjectInfo(this, activeChar));
		}
		else
		{
			activeChar.sendPacket(new AbstractNpcInfo.NpcInfo(this, activeChar));
		}
	}
	
	public void showNoTeachHtml(MMOPcInstance player)
	{
		int npcId = getNpcId();
		String html = "";
		
		if (this instanceof MMOWarehouseInstance)
		{
			html = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/warehouse/" + npcId + "-noteach.htm");
		}
		else if (this instanceof MMOTrainerHealersInstance)
		{
			html = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/trainer/skilltransfer/" + npcId + "-noteach.htm");
		}
		else if (this instanceof MMOTrainerInstance)
		{
			html = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/trainer/" + npcId + "-noteach.htm");
		}
		
		if (html == null)
		{
			_log.warning("Npc " + npcId + " missing noTeach html!");
			NpcHtmlMessage msg = new NpcHtmlMessage(getObjectId());
			final String sb = StringUtil.concat("<html><body>" + "I cannot teach you any skills.<br>You must find your current class teachers.", "</body></html>");
			msg.setHtml(sb);
			player.sendPacket(msg);
			return;
		}
		
		final NpcHtmlMessage noTeachMsg = new NpcHtmlMessage(getObjectId());
		noTeachMsg.setHtml(html);
		noTeachMsg.replace("%objectId%", String.valueOf(getObjectId()));
		player.sendPacket(noTeachMsg);
	}
	
	public MMONpc scheduleDespawn(long delay)
	{
		ThreadPoolManager.getInstance().scheduleGeneral(new DespawnTask(), delay);
		return this;
	}
	
	protected class DespawnTask implements Runnable
	{
		@Override
		public void run()
		{
			if (!isDecayed())
			{
				deleteMe();
			}
		}
	}
	
	@Override
	protected final void notifyQuestEventSkillFinished(MMOSkill skill, MMOObject target)
	{
		try
		{
			if (getTemplate().getEventQuests(QuestEventType.ON_SPELL_FINISHED) != null)
			{
				MMOPcInstance player = null;
				if (target != null)
				{
					player = target.getActingPlayer();
				}
				for (Quest quest : getTemplate().getEventQuests(QuestEventType.ON_SPELL_FINISHED))
				{
					quest.notifySpellFinished(this, player, skill);
				}
			}
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE, "", e);
		}
	}
	
	@Override
	public boolean isMovementDisabled()
	{
		return super.isMovementDisabled() || (getCanMove() == 0) || getAiType().equals(AIType.CORPSE);
	}
	
	public AIType getAiType()
	{
		return _staticAIData.getAiType();
	}
	
	public void setDisplayEffect(int val)
	{
		if (val != _displayEffect)
		{
			_displayEffect = val;
			broadcastPacket(new ExChangeNpcState(getObjectId(), val));
		}
	}
	
	public int getDisplayEffect()
	{
		return _displayEffect;
	}
	
	public int getColorEffect()
	{
		return 0;
	}
	
	/**
	 * @return the character that summoned this NPC.
	 */
	public MMOCharacter getSummoner()
	{
		return _summoner;
	}
	
	/**
	 * @param summoner the summoner of this NPC.
	 */
	public void setSummoner(MMOCharacter summoner)
	{
		_summoner = summoner;
	}
	
	@Override
	public boolean isNpc()
	{
		return true;
	}
	
	@Override
	public void setTeam(int id)
	{
		super.setTeam(id);
		for (MMOPcInstance player : getKnownList().getKnownPlayers().values())
		{
			player.sendPacket(new AbstractNpcInfo.NpcInfo(this, player));
		}
	}
	
	@Override
	public boolean isWalker()
	{
		return WalkingManager.getInstance().isRegistered(this);
	}
	
	@Override
	public boolean isChargedShot(ShotType type)
	{
		return (_shotsMask & type.getMask()) == type.getMask();
	}
	
	@Override
	public void setChargedShot(ShotType type, boolean charged)
	{
		if (charged)
		{
			_shotsMask |= type.getMask();
		}
		else
		{
			_shotsMask &= ~type.getMask();
		}
	}
	
	@Override
	public void rechargeShots(boolean physical, boolean magic)
	{
		if ((_soulshotamount > 0) || (_spiritshotamount > 0))
		{
			if (physical)
			{
				if (_soulshotamount == 0)
				{
					return;
				}
				else if (Rnd.get(100) > getSoulShotChance())
				{
					return;
				}
				_soulshotamount--;
				Broadcast.toSelfAndKnownPlayersInRadius(this, new MagicSkillUse(this, this, 2154, 1, 0, 0), 600);
				setChargedShot(ShotType.SOULSHOTS, true);
			}
			if (magic)
			{
				if (_spiritshotamount == 0)
				{
					return;
				}
				else if (Rnd.get(100) > getSpiritShotChance())
				{
					return;
				}
				_spiritshotamount--;
				Broadcast.toSelfAndKnownPlayersInRadius(this, new MagicSkillUse(this, this, 2061, 1, 0, 0), 600);
				setChargedShot(ShotType.SPIRITSHOTS, true);
			}
		}
	}
	
	public int getScriptValue()
	{
		return _scriptVal;
	}
	
	public void setScriptValue(int val)
	{
		_scriptVal = val;
	}
	
	public boolean isScriptValue(int val)
	{
		return _scriptVal == val;
	}
	
	/**
	 * Send an "event" to all NPC's within given radius
	 * @param eventName - name of event
	 * @param radius - radius to send event
	 * @param reference - MMOObject to pass, if needed
	 */
	public void broadcastEvent(String eventName, int radius, MMOObject reference)
	{
		for (MMOObject obj : MMOWorld.getInstance().getVisibleObjects(this, radius))
		{
			if (obj.isNpc() && (((MMONpc) obj).getTemplate().getEventQuests(QuestEventType.ON_EVENT_RECEIVED) != null))
			{
				for (Quest quest : ((MMONpc) obj).getTemplate().getEventQuests(QuestEventType.ON_EVENT_RECEIVED))
				{
					quest.notifyEventReceived(eventName, this, (MMONpc) obj, reference);
				}
			}
		}
	}
}
